到上一篇文章為止,TDD所需要的每個片段都已經簡單介紹了一遍,相信各位讀者也很清楚的瞭解,筆者要表達的重點,還是一句話:一切都為了滿足使用者需求。
接下來,筆者透過一個簡單的例子,從實作順序面來介紹,怎麼從一個使用者需求開始,到一個循環結束(驗收測試案例可能不夠完整,但循環是一致的)
上一篇文章:[Day 26]User Story/ATDD/BDD/TDD - 總結
本系列文章專區
@範例介紹
這個例子的背景,是一個網路銀行的系統。
而這邊選用的user story,是登入的功能。因為大部分的開發人員做過的系統,應該都有登入的功能,即使沒開發過,至少也使用過。希望透過這樣的例子,讀者會比較好理解,比較有共鳴。
當然因為這不是真實的系統,所以防呆面或需求面可能不夠完善,這邊就先跟讀者們說聲抱歉。
@定義需求
PO:「嘿,我們的系統,應該要有個登入的功能。當使用者進到系統中,若還沒有經過登入頁面驗證身份的話,要先將使用者導到登入頁。登入成功之後,再導到我們的首頁。」
依照PO的說法,與PO討論之後,PO,測試人員與開發人員,決定用一個user story來描述這樣的一個需求:
「我們需要一個登入的功能:
In order to 驗證身份,避免非法使用者使用系統
As a 線上使用者
I want to 驗證使用者身份是否合法」
如同上一篇文章所介紹的TDD開發流程所說,當我們建立了一個user story之後,接下來就是:
@建立測試專案
首先建立一個測試專案,命名為「TestWebBank」。如下圖所示:
在測試專案中,透過NuGet加入幾個在TDD中需要用到的參考:
並將SpecFlow的App.Config中的設定,改成使用MSTest來執行。如下圖所示:
@建立Login的Feature檔
在測試專案中,加入一個Login.Feature檔。如下圖所示:
將user story的部分,填入feature中,如下圖所示:
@確認畫面
確定了user story之後,接著測試人員與開發人員,協同PO一起討論,該怎麼驗收這個user story。
通常PO或使用者,需要透過UI畫面或雛形系統,才比較容易確認,這樣子是不是他們要的功能。因此,可以透過白板、紙筆、Word、PowerPoint、建立prototype/mockup的工具(例如 Balsamiq Mockups、Moqups、axure)來輔助,迅速地確認這樣的畫面,是否為使用者希望有的功能。
這邊的例子,是開發人員迅速建立一個網站專案,並做了一個只有樣子,但沒有穿衣服的html網頁,上面只有兩個輸入項,分別是:
以及一個「確認」的登入按鈕。
.aspx程式碼:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<form id="form1" runat="server">
<div>
提款卡ID:
<asp:TextBox ID="txtCardId" runat="server"></asp:TextBox><br />
密碼:
<asp:TextBox ID="txtPassword" runat="server" TextMode="Password"></asp:TextBox><br />
<asp:Button ID="btnLogin" runat="server" Text="確認" />
</div>
</form>
畫面如下圖所示:
(開發人員/測試人員:當然畫面不會這麼醜,但基本上是不是畫面只需要有這些輸入項即可?)
確認畫面無誤後,接下來要來定義登入功能中,應該要有的系統行為。
@建立驗收測試案例
討論後,先定義出登入功能應該要具備下面幾項功能:
在user story card背後,寫上這兩點驗收測試案例之後,接下來我們先在SpecFlow的feature檔中,將這兩個scenario補上去。如下圖所示:
筆者建議在描述scenario的時候,就應該要有擬真的input/output資料,這樣才會比較貼近驗收測試的情況。
而有了這樣的scenario/acceptance test cases,也可以方便我們先行準備測試資料。
接著透過SpecFlow自動產生step的功能,幫我們產生Login.feature所對應的step檔案內容。如下圖所示:
Step的程式碼如下:
using System;
using TechTalk.SpecFlow;
namespace TestWebBank
{
[Binding]
public class 登入功能Steps
{
[Given(@"在登入頁面")]
public void Given在登入頁面()
{
ScenarioContext.Current.Pending();
}
[Given(@"提款卡Id輸入""(.*)""")]
public void Given提款卡Id輸入(int p0)
{
ScenarioContext.Current.Pending();
}
[Given(@"密碼輸入""(.*)""")]
public void Given密碼輸入(int p0)
{
ScenarioContext.Current.Pending();
}
[When(@"按下確認按鈕")]
public void When按下確認按鈕()
{
ScenarioContext.Current.Pending();
}
[Then(@"頁面url為""(.*)""")]
public void Then頁面Url為(string p0)
{
ScenarioContext.Current.Pending();
}
[Then(@"呈現訊息為""(.*)""")]
public void Then呈現訊息為(string p0)
{
ScenarioContext.Current.Pending();
}
}
}
到這邊,user story, acceptance test cases, feature, scenario都定義好了,我們也已經建立好測試專案與網站專案了。
接下來就要開始撰寫驗收測試程式了。
@撰寫驗收測試程式 - Selenium
我們已經有了簡單的網頁,也有了期望的scenario,接下來測試人員就可以開始撰寫自動化的驗收測試程式了。
這邊筆者建議,如果測試人員對Selenium的library還不夠熟悉時,開發人員可以先給點幫助。例如先hard-code寫出兩種結果:
hard-code程式碼如下:
protected void btnLogin_Click(object sender, EventArgs e)
{
//密碼驗證錯誤
//this.Message.Text = @"密碼輸入錯誤";
//密碼驗證成功
//Response.Redirect("index.aspx");
}
讓測試人員/開發人員,可以先自行透過Selenium IDE來錄製Selenium腳本。
這邊舉「驗證成功後,要導到index.aspx」為例。
Selenium錄製腳本,如下圖所示:
錄製輸入資料:
導到index頁面:
這邊別忘了,我們還要驗證「是否導到了index.aspx」,這裡筆者先加上註解就好,因為最後是要透過WebDriver去做驗證。最後將此scenario存成loginSuccess,當然最好的方式是,存成跟scenario可以直接對照的檔名。
依此類推,將密碼輸入錯誤,驗證失敗的腳本也錄製好。如下圖所示:
@Export Selenium Test Cases to Selenium.WebDriver Code
如同前面文章:[Day 8]Integration Testing & Web UI Testing所提到,我們將錄好的selenium test cases,透過export轉成C# with NUnit的code。如下圖所示:
程式碼如下所示:
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;
namespace SeleniumTests
{
[TestFixture]
public class LoginSuccess
{
private IWebDriver driver;
private StringBuilder verificationErrors;
private string baseURL;
[SetUp]
public void SetupTest()
{
driver = new FirefoxDriver();
baseURL = "http://localhost:10542/";
verificationErrors = new StringBuilder();
}
[TearDown]
public void TeardownTest()
{
try
{
driver.Quit();
}
catch (Exception)
{
// Ignore errors if unable to close the browser
}
Assert.AreEqual("", verificationErrors.ToString());
}
[Test]
public void TheLoginSuccessTest()
{
driver.Navigate().GoToUrl(baseURL + "/WebBankSite/Login.aspx");
driver.FindElement(By.Id("txtCardId")).Clear();
driver.FindElement(By.Id("txtCardId")).SendKeys("1234");
driver.FindElement(By.Id("txtPassword")).Clear();
driver.FindElement(By.Id("txtPassword")).SendKeys("91");
driver.FindElement(By.Id("btnLogin")).Click();
// 驗證url是否為index.aspx
}
private bool IsElementPresent(By by)
{
try
{
driver.FindElement(by);
return true;
}
catch (NoSuchElementException)
{
return false;
}
}
}
}
有了這樣的Selenium自動測試程式,瞭解每一行程式碼的內容之後,我們只需要依照我們所定義好SpecFlow的Scenario,在Steps中,把對應的動作,放進去每一個scenario的關鍵字function中即可。(這邊因為使用的是MSTest,因此一些語法也要做點小修改。)
修改完成的step內容,程式碼如下所示:
using System;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using TechTalk.SpecFlow;
namespace TestWebBank
{
[Binding]
public class 登入功能Steps
{
#region Test Setting
private static IWebDriver driver;
private static StringBuilder verificationErrors;
private static string baseURL;
[BeforeFeature("WebBank")]
public static void BeforeFeatureWebAtm()
{
driver = new FirefoxDriver();
//請自行修改為網站的domain name與port
baseURL = "http://localhost:10542";
verificationErrors = new StringBuilder();
}
[AfterFeature("WebBank")]
public static void AfterFeatureWebAtm()
{
try
{
driver.Quit();
}
catch (Exception)
{
// Ignore errors if unable to close the browser
}
Assert.AreEqual("", verificationErrors.ToString());
}
#endregion Test Setting
[Given(@"在登入頁面")]
public void Given在登入頁面()
{
driver.Navigate().GoToUrl(baseURL + "/WebBankSite/Login.aspx");
}
[Given(@"提款卡Id輸入""(.*)""")]
public void Given提款卡Id輸入(string cardId)
{
driver.FindElement(By.Id("txtCardId")).Clear();
driver.FindElement(By.Id("txtCardId")).SendKeys(cardId);
}
[Given(@"密碼輸入""(.*)""")]
public void Given密碼輸入(string password)
{
driver.FindElement(By.Id("txtPassword")).Clear();
driver.FindElement(By.Id("txtPassword")).SendKeys(password);
}
[When(@"按下確認按鈕")]
public void When按下確認按鈕()
{
driver.FindElement(By.Id("btnLogin")).Click();
}
[Then(@"頁面url為""(.*)""")]
public void Then頁面Url為(string url)
{
var expected = string.Format("{0}/WebBankSite/{1}", baseURL, url);
Assert.AreEqual(expected, driver.Url);
}
[Then(@"呈現訊息為""(.*)""")]
public void Then呈現訊息為(string message)
{
Assert.AreEqual(message, driver.FindElement(By.Id("Message")).Text);
}
}
}
@執行Scenario的測試
既然測試的程式碼都寫完了,讓我們來執行一下測試,看一下測試的結果。
(註:執行Selenium WebDriver的測試時間可能會比較久一點點,因為要透過WebDriver啟動Firefox,並且執行相關Selenium test cases。若讀者需要測試其他瀏覽器,只要參考對應browser的WebDriver即可)
可以看到兩個測試都失敗了,錯誤訊息分別為:
LoginSuccess: Assert.AreEqual 失敗。預期: http://localhost:10542/WebBankSite/index.aspx。實際: http://localhost:10542/WebBankSite/Login.aspx。
LoginFailed: Assert.AreEqual 失敗。預期: <密碼輸入錯誤>。實際: <>。
紅燈!這就是整個ATDD的第一個階段:紅燈。
@小結
為了避免篇幅太長,這篇文章到這邊,就只先介紹了:
下一篇文章,就比較簡單一點了,我們只要想辦法讓紅燈變成綠燈即可。